/*
        Copyright 2018 Gaurav Kumar

        Licensed under the Apache License, Version 2.0 (the "License");
        you may not use this file except in compliance with the License.
        You may obtain a copy of the License at

        http://www.apache.org/licenses/LICENSE-2.0

        Unless required by applicable law or agreed to in writing, software
        distributed under the License is distributed on an "AS IS" BASIS,
        WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
        See the License for the specific language governing permissions and
        limitations under the License.
*/

package com.gauravk.audiovisualizer.visualizer;

import com.gauravk.audiovisualizer.base.BaseVisualizer;
import com.gauravk.audiovisualizer.model.AnimSpeed;
import com.gauravk.audiovisualizer.model.PaintStyle;
import com.gauravk.audiovisualizer.model.PositionGravity;
import com.gauravk.audiovisualizer.utils.AVConstants;

import ohos.agp.components.AttrSet;
import ohos.agp.components.Component;
import ohos.agp.render.Canvas;
import ohos.agp.render.Path;
import ohos.agp.utils.Point;
import ohos.agp.utils.Rect;
import ohos.app.Context;

import org.jetbrains.annotations.Nullable;

import java.security.SecureRandom;
import java.util.Random;

/**
 * Custom view to create wave visualizer
 * <p>
 * Created by gk
 */
public class WaveVisualizer extends BaseVisualizer implements Component.DrawTask {
    private static final int WAVE_MAX_POINTS = 54;
    private static final int WAVE_MIN_POINTS = 3;

    private int mMaxBatchCount;

    private Path mWavePath;

    private int nPoints;

    private Point[] mBezierPoints;
    private Point[] mBezierControlPoints1;
    private Point[] mBezierControlPoints2;

    private float[] mSrcY;
    private float[] mDestY;

    private float mWidthOffset;
    private Rect mClipBounds;

    private int nBatchCount;

    private SecureRandom mRandom = new SecureRandom();

    /**
     * 使用java代码实例化调用
     *
     * @param context
     */
    public WaveVisualizer(Context context) {
        super(context);
    }

    /**
     * 使用xml布局引用调用
     *
     * @param context
     * @param attrs
     */
    public WaveVisualizer(Context context,
                          @Nullable AttrSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void init() {
        addDrawTask(this::onDraw);
        nPoints = (int) (WAVE_MAX_POINTS * mDensity);
        if (nPoints < WAVE_MIN_POINTS) {
            nPoints = WAVE_MIN_POINTS;
        }

        mWidthOffset = -1;
        nBatchCount = 0;

        setAnimationSpeed(mAnimSpeed);

        mClipBounds = new Rect();

        mWavePath = new Path();

        mSrcY = new float[nPoints + 1];
        mDestY = new float[nPoints + 1];

        // initialize mBezierPoints
        mBezierPoints = new Point[nPoints + 1];
        mBezierControlPoints1 = new Point[nPoints + 1];
        mBezierControlPoints2 = new Point[nPoints + 1];
        for (int i = 0; i < mBezierPoints.length; i++) {
            mBezierPoints[i] = new Point();
            mBezierControlPoints1[i] = new Point();
            mBezierControlPoints2[i] = new Point();
        }
    }

    @Override
    public void setAnimationSpeed(AnimSpeed animSpeed) {
        super.setAnimationSpeed(animSpeed);
        this.mMaxBatchCount = AVConstants.MAX_ANIM_BATCH_COUNT - mAnimSpeed.ordinal();
    }

    @Override
    public void onDraw(Component component, Canvas canvas) {
        onDrawInit(component, canvas);

        // create the path and draw
        if (isVisualizationEnabled && mRawAudioBytes != null) {
            if (mRawAudioBytes.length == 0) {
                return;
            }

            mWavePath.rewind();

            onDrawModification(component);

            // increment batch count
            nBatchCount++;

            onDrawPainting();
            canvas.drawPath(mWavePath, mPaint);
        }
        super.addDrawTask(this::onDraw);
    }

    private void onDrawPainting() {
        // for smoothing animation
        for (int i = 0; i < mBezierPoints.length; i++) {
            mBezierPoints[i].modify(mBezierPoints[i].getPointX(), mSrcY[i] + (((float) (nBatchCount) / mMaxBatchCount) * (mDestY[i] - mSrcY[i])));
        }

        // reset the batch count
        if (nBatchCount == mMaxBatchCount) {
            nBatchCount = 0;
        }

        // calculate the bezier curve control points
        for (int i = 1; i < mBezierPoints.length; i++) {
            mBezierControlPoints1[i].modify((mBezierPoints[i].getPointX() + mBezierPoints[i - 1].getPointX()) / 2, mBezierPoints[i - 1].getPointY());
            mBezierControlPoints2[i].modify((mBezierPoints[i].getPointX() + mBezierPoints[i - 1].getPointX()) / 2, mBezierPoints[i].getPointY());
        }

        // create the path
        mWavePath.moveTo(mBezierPoints[0].getPointX(), mBezierPoints[0].getPointY());
        for (int i = 1; i < mBezierPoints.length; i++) {
            mWavePath.cubicTo(mBezierControlPoints1[i].getPointX(), mBezierControlPoints1[i].getPointY(),
                    mBezierControlPoints2[i].getPointX(), mBezierControlPoints2[i].getPointY(),
                    mBezierPoints[i].getPointX(), mBezierPoints[i].getPointY());
        }

        if (mPaintStyle == PaintStyle.FILL) {
            mWavePath.lineTo(mClipBounds.right, mClipBounds.bottom);
            mWavePath.lineTo(mClipBounds.left, mClipBounds.bottom);
            mWavePath.close();
        }
    }

    private void onDrawModification(Component component) {
        // find the destination bezier point for a batch
        if (nBatchCount == 0) {
            float randPosY = mDestY[mRandom.nextInt(nPoints)];
            for (int i = 0; i < mBezierPoints.length; i++) {
                int x = (int) Math.ceil((i + 1) * (mRawAudioBytes.length / nPoints));

                int t = 0;
                if (x < 1024) {
                    t = component.getHeight()
                            + ((byte) (Math.abs(mRawAudioBytes[x]) + 128)) * component.getHeight() / 128;
                }

                float posY;
                if (mPositionGravity == PositionGravity.TOP) {
                    posY = mClipBounds.bottom - t;
                } else {
                    posY = mClipBounds.top + t;
                }

                //change the source and destination y
                mSrcY[i] = mDestY[i];
                mDestY[i] = posY;
            }

            mDestY[mBezierPoints.length - 1] = randPosY;
        }
    }

    private void onDrawInit(Component component, Canvas canvas) {
        if (mWidthOffset == -1) {
            canvas.getLocalClipBounds(mClipBounds);

            mWidthOffset = component.getWidth() / nPoints;

            //initialize bezier points
            for (int i = 0; i < mBezierPoints.length; i++) {
                float posX = mClipBounds.left + (i * mWidthOffset);

                float posY;
                if (mPositionGravity == PositionGravity.TOP) {
                    posY = mClipBounds.top;
                } else {
                    posY = mClipBounds.bottom;
                }

                mSrcY[i] = posY;
                mDestY[i] = posY;
                mBezierPoints[i].modify(posX, posY);
            }
        }
    }
}